Fix session stuck in Stopping state when interrupt hangs#334
Fix session stuck in Stopping state when interrupt hangs#334brendanlong wants to merge 2 commits intomainfrom
Conversation
The SDK's interrupt() and close() calls can hang indefinitely if the agent process is already dead or unresponsive. This causes the stop mutation to never complete, leaving the UI stuck on "Stopping...". Changes: - Add 5-second timeout to interruptClaude() so it doesn't hang forever - Add forceCleanSession() to clean up in-memory state when SDK calls fail or time out - Add onError handlers to stop/interrupt mutations on the client so the UI recovers by refetching actual state instead of staying stuck Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces timeout handling and robust state recovery for Claude session interruptions. Key changes include adding onError handlers to frontend hooks to refetch state on failure, and implementing a withTimeout mechanism in the backend to force-clean session state if the SDK's interrupt call hangs or fails. Review feedback identifies a logic bug in the withTimeout implementation where the success of a void promise is indistinguishable from a timeout, and notes a potential resource leak due to un-cleared timers.
| function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> { | ||
| return Promise.race([ | ||
| promise, | ||
| new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), ms)), | ||
| ]); | ||
| } |
There was a problem hiding this comment.
The current implementation of withTimeout has two issues:
- Ambiguity: It resolves to
undefinedboth when the timeout occurs and when the input promise resolves toundefined. Sincestate.currentQuery.interrupt()returnsPromise<void>, it always resolves toundefinedon success, making the timeout check at line 631 always true. - Resource Leak: The
setTimeouttimer is not cleared if the promise resolves before the timeout, which can lead to an accumulation of active timers in a long-running process.
I suggest using a wrapper object to distinguish between success and timeout, and ensuring the timer is cleared.
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<{ ok: true; value: T } | { ok: false }> {
let timeoutId: ReturnType<typeof setTimeout>;
const timeoutPromise = new Promise<{ ok: false }>((resolve) => {
timeoutId = setTimeout(() => resolve({ ok: false }), ms);
});
return Promise.race([
promise.then((value) => ({ ok: true as const, value })),
timeoutPromise,
]).finally(() => clearTimeout(timeoutId));
}| const result = await withTimeout(state.currentQuery.interrupt(), INTERRUPT_TIMEOUT_MS); | ||
| if (result === undefined) { |
There was a problem hiding this comment.
Update this check to align with the improved withTimeout implementation that uses a wrapper object to distinguish between a successful interruption and a timeout.
| const result = await withTimeout(state.currentQuery.interrupt(), INTERRUPT_TIMEOUT_MS); | |
| if (result === undefined) { | |
| const result = await withTimeout(state.currentQuery.interrupt(), INTERRUPT_TIMEOUT_MS); | |
| if (!result.ok) { |
When interrupt() times out, forceCleanSession was only cleaning up in-memory state but leaving the CLI subprocess running. Now it calls close() first, which triggers the SDK's subprocess kill chain (SIGTERM immediately, SIGKILL after 5s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
interrupt()orclose()calls hangcfe89d5c-9ebf-4240-bf7c-7b6ae2ef6da9was manually fixed in the prod DBinterruptClaude()with fallback to force-clean the in-memory stateonErrorhandlers to the client-side stop/interrupt mutations so the UI recovers by refetching state instead of showing "Stopping..." foreverRoot cause
The SDK's
interrupt()can hang indefinitely if the agent is already dead or unresponsive. Sinceclaude.interruptawaits this call, the HTTP request never completes, and the client's mutation stays inisPendingstate forever.Test plan
🤖 Generated with Claude Code